<template>
  <div class="wrapper" @scroll.passive="openVirtualScroll ? scrollHandler() : undefined" ref="wrapperRef">
   
    <div :style="openVirtualScroll ? blankFillStyle : undefined">
      <table @contextmenu.prevent="emits('contextmenuRight')">
        <slot name="thead"></slot>
        <tbody>
          <template v-for="(item, index) in openVirtualScroll ? showDataList : sourceDataList" :key="item[rowKey] || data.startIndex + index">
            <slot :item="sourceDataList[data.startRowIndex + index]" :rowindex="data.startRowIndex + index"></slot>
          </template>
        </tbody>
      </table>
    </div>
  </div>
</template>
<script setup>
import { reactive, onMounted, computed, ref } from "vue"
const props = defineProps({
  rowKey: {
    type: String,
    default: "id",
  },
  openVirtualScroll: {
    type: Boolean,
    default: false
  },
  // 一条数据内容的高度
  oneDataHeight: {
    type: Number,
    default: 0
  },
  // 是否向外触发滚动至底事件(scroll-last)
  scrollLastFlag: {
    type: Boolean,
    default: false
  },
  // 是否向外触发滚动事件(scroll)，会传出滚动的位移
  scrollFlag: {
    type: Boolean,
    default: false
  },
  // 屏幕上方留几条缓冲数据，以消除因向下滚动过快而出现的白屏现象
  visibleBeforeCount: {
    type: Number,
    default: 10
  },
  // 屏幕下方留几条缓冲数据，以消除因向上滚动过快而出现的白屏现象
  visibleAfterCount: {
    type: Number,
    default: 10
  }
})
const sourceDataList = defineModel('sourceDataList', {default: () => [] })
const emits = defineEmits(["contextmenuRight", "scroll", "scroll-last"])

const data = reactive({
  // 可视屏幕容积数量
  screenContainSize: 0,
  // 当前数据起始位置索引
  startIndex: 0,
  // 滚动事件触发执行的函数
  scrollFn: null,
  // 保存滚动位移
  scrollY: 0,
  // 渲染的一批数据的起始index
  startRowIndex: 0,
})

const endIndex = computed(() => {
  let endIndex = data.startIndex + data.screenContainSize + props.visibleAfterCount;
  // 如果endIndex位置索引数据不存在，则就等于源数据的长度
  if(!sourceDataList.value[endIndex]){
    endIndex = sourceDataList.value.length;
  }
  return endIndex;
})
// 渲染的一批数据
const showDataList = computed(() => {
  // 屏幕上方留一屏缓冲数据，以消除因向下滚动过快而出现的白屏现象
  let startIndex = data.startIndex;
  if(startIndex < data.screenContainSize){
    startIndex = 0;
  }else{
    startIndex = startIndex - props.visibleBeforeCount;
  }

  if(startIndex < 0) startIndex = 0;

  data.startRowIndex = startIndex;
  
  // 截取要展示的数据
  return sourceDataList.value.slice(startIndex, endIndex.value)
})
const blankFillStyle = computed(() => {
  // 上方因为留了一屏缓冲数据，因此滚动过一屏数据后才开始计算上方空白占位
  let startIndex = data.startIndex;
  if(startIndex < data.screenContainSize){
    startIndex = 0;
  }else{
    startIndex = data.startIndex - props.visibleBeforeCount;
  }

  return{
    paddingTop: startIndex * props.oneDataHeight + 'px',
    paddingBottom: (sourceDataList.value.length - endIndex.value) * props.oneDataHeight + 'px'
  }
})

const wrapperRef = ref(null)
onMounted(() => {
  // 挂载后，根据可视容器高度计算可视屏幕容积数量
  myResize()
  // 屏幕尺寸变化以及横屏，都要重新计算可视屏幕容积数量
  window.onresize = myResize;
  window.onorientationchange = myResize;
})

function myResize(){
  // 两次取反可取整，上下有空位，因此需要加2条数据
  data.screenContainSize = ~~(wrapperRef.value.offsetHeight / props.oneDataHeight) + 2;
}

// 定时器节流函数
function throttle(fn, delay){
  let timer = null;
  return function(...args){
    if(!timer){
      timer = setTimeout(()=>{
        fn.apply(this, args);
        clearTimeout(timer);
        timer = null;
      }, delay);
    }
  }
}
// 滚动事件处理
function scrollHandler(){
  const fps = 30; //屏幕刷新率为30hz
  const interval = parseInt(1000 / fps); //每次的时间间隔
  let then = Date.now();
  // 定义请求动画帧回调函数
  const callback = () => {
    const now = Date.now();
    setStartIndex();
    // 兼容低刷新率设备，如果屏幕刷新率低于30hz，递归执行回调函数
    if(now - then >= interval){
      then = now;
      window.requestAnimationFrame(callback);
    }
  }
  window.requestAnimationFrame(callback);
}

// 根据滚动的位移计算当前数据起始位置索引
function setStartIndex(){
  data.scrollY = wrapperRef.value.scrollTop;
  if(props.scrollFlag){
    emits('scroll', data.scrollY);
  }
  let currentIndex = ~~(data.scrollY / props.oneDataHeight);
  // 如果上一次的startIndex与现在的startIndex相等，直接返回，无须处理
  if(currentIndex === data.startIndex) return;
  data.startIndex = currentIndex;
  // 滚动至底向外触发事件
  if(endIndex.value >= sourceDataList.value.length && props.scrollLastFlag){
    emits('scroll-last');
  }
}

// 设置滚动到具体的位置,delay：过渡时间
function scrollTo(val, delay = 0){
  if(delay === 0) {
    wrapperRef.value.scrollTop = val;
    return;
  }
  const ms = Math.ceil(delay / 5);
  const timer = setInterval(() => {
    const scrollTop = wrapperRef.value.scrollTop;
    const speed = Math.ceil((scrollTop - val) / 5);
    wrapperRef.value.scrollTop = scrollTop - speed <= val ? val : scrollTop - speed;
    if(wrapperRef.value.scrollTop === val){
      clearInterval(timer);
    }
  }, ms);
}

// 获取滚动位移
function scrollOffset(){
  return data.scrollY;
}

defineExpose({
  scrollTo,
  scrollOffset
})
</script>
<style scoped>
.wrapper{
  overflow-y: auto;
}
</style>